记一次pnpm monorepo实践
入职新公司快一个月了。再一次的东京生活也算稳定下来了。到现在为止生活还算惬意,30 岁之后心态真的会发生很大的变化(笑


每当天气好的清晨黄昏,就可以在小屋阳台看到富士山
话不多说,接手的第一个项目是类似问卷的项目,包括管理侧制作问卷,和用户侧使用问卷。分两个仓库开发过一版,但是种种原因没有上线。现在时机成熟了,重启项目。新的一版设计中管理侧有预览用户侧问卷的功能,那为什么不把两个仓库合二为一呢。
上一份工作有大规模的使用 Monorepo,采用的是微软开源的NX作为构建 Monorepo 的方案,功能很齐全很强大,它的命令行工具类似 NG CLI,但这样带来的副作用就是刚上手可能会有一些不知所措。小型项目使用的话,有些杀鸡用牛刀。
短暂的考虑一番之后,决定采用简单轻量pnpm workspace方案,花了一天时间把两个项目合而为一,顺便替换掉Yarn.
由于基于原 admin 仓库进行改造的,在项目根目录创建pnpm-workspace.yaml文件,确定 workspace 位置。我这边的初步设计很简单就是 两个 App 分别是: admin 和 user,公共部分放在 packages 文件夹里
项目结构如下
.
├── apps
│ │ ├── admin (Admin app)
│ │ ├── user (User app)
├── packages (Common)
仅此pnpm-workspace.yaml配置如下(暴露了两个文件夹)
packages:
- 'packages/*'
- 'apps/*'
三部分就是三个独立的前端项目。
- 之前 admin 代码内容统统移动到 apps/admin 里面。合并的 user 仓库代码夜一并移动到 apps/user 中。
公共部分 packages 和最外层 也
pnpm init分别创建package.json。 - 调整
package.json中 name(也就是包名)现在四个 workspace 区域。最好起名相互管理。这里最外层叫lacApp,里层三个项目分别交@lacApp/admin/@lacApp/user以及@lacApp/packages - 删除调整代码:之前移动过去的 admin、user 代码。删除 lock file,ignore file(之后在外层统一管理)。
.eslintrc/tsconfig如果用共同,推荐在外层维护一个共通的配置,里层项目通过 extend 方式继承调整配置代码 4.公共部分 npm 包移动到最外层。比如react/eslint/axios/vite/typescript··这些两个 app 或者公共包都会用的移动到最外层,相应的里层package.json中进行删除。
如果之后需要全局安装以来,可以通过 -w、--workspace-root 参数来安装. 给某个特定的 package 安装依赖,可以通过 --filter 参数来安装.
eg:
# 全局安装开发依赖eslint
pnpm install eslint -wD
# 特定的 package 安装依赖
pnpm add dayjs --filter @lacApp/admin
模块之间进行相互依赖,比如我们的公共部分@lacApp/packages,分别在 admin 和 user 中使用。它作为一个包,作为 app 的依赖
pnpm add @lacApp/packages -r --filter @lacApp/admin
4.调整好后,最外层添加 file:.npmrc
auto-install-peers = true
告诉npm在安装一个包的同时,自动安装其在 monorepo 中的相互依赖(peer dependencies)。
5.最外层pnpm i 就可以了。(是不是很简单,在外层生成一份 lock 文件。并且里层的 npm 包也一并进行了安装
6.调整 script。其实 cd 到里层项目执行脚本完全 ok。但是这里更推荐。外层去配置相应的 script 统一管理 比如:
### dev
pnpm -r --filter=@lacApp/admin run dev
pnpm -r --filter=@lacApp/user run dev
###如果想要一起执行也是可以的(默认情况下,pnpm 会根据子包的依赖拓扑排序,按顺序对子包执行命令,以避免在构建某个包的时候,出现子依赖的构建产物未生成的问题,进而引发比如类型错误等问题。另外如果两个子包没有依赖关系,pnpm 会并发进行构建。
pnpm -r --filter=./apps/* run dev
7.其实到这一步就算完了,是不是很简单。构建配置需要和运维进行简单的沟通。可能之前 dist 目录在根目录现在无非在 apps/admin/dist和 apps/user/dist多了两层
8.* 上车turborepo.其实其实到第七步就算完了,是不是很简单。为了更好的 monorepo 构建体验(想折腾一下)可以上车Turborepo.
turborepo 是一个适用于 JavaScript 和 Typescript monorepo 的高性能构建工具,它不是一个侵入式的工具,你可以在项目中渐进的引入和使用它,它通过足够的封装度,使用一些简单的配置来达到高性能的项目构建。 和 esbuild 一样,Turborepo 也是基于 go/rust 实现的工具,在语言层面上就具有一定的性能优势。还有更快的增量构建。turborepo 中的缓存机制 可以帮助我们记住构建内容 并且跳过已经计算过的内容,优化打包效率。(真的超级快
turborepo 正因为它可以渐进的引入和使用,改造起来很简单。
pnpm install turbo -w -D
之后在根目录配置 turbo.json.这里有个pipeline 管道的概念,
说简单点就是基于turbo.json约定的配置,控制脚本执行的顺序、依赖。按配置文件定义任务之间的关系,然后让 Turborepo 优化构建内容和时间
在我们执行turbo run命令的时候turbo会根据我们定义的 Pipelines(https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) 里对命令的各种配置去对我们的每个 package 中的 package.json 中 对应的 script 执行脚本进行有序的执行和缓存输出的文件。
当然 turbo 里的 pipeline name 也是 monorepo 中每一个子包里的script。比如 admin app 和 user app 都有run dev命令。这里就可以定义一个 name 为 dev 的 Pipelines.执行 script:turbo run dev就会同时执行 两个 app 中定义的dev命令
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// A workspace's `build` task depends on that workspace's
// topological dependencies' and devDependencies'
// `build` tasks being completed first. The `^` symbol
// indicates an upstream dependency.
// 可以通过^符号来显式声明该任务具有拓扑依赖性,需要依赖的包执行完相应的任务后才能开始执行自己的任务
// 比如我们的admin项目引用了@lacApp/package.便构成了依赖关系,根据build的dependsOn配置,
// 会先执行依赖项的build命令,也就是@lacApp/package.的build命令,依赖项执行完后才会执行@lacApp/package的build命令。
// 如果我们不添加"dependsOn": ["^build"]数组中的‘^’那么就代表我们当前只需要执行我们自己的build命令
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", ".svelte-kit/**"]
},
"deploy": {
// A workspace's `deploy` task depends on the `build`,
// `test`, and `lint` tasks of the same workspace
// being completed.
// 如果一个任务的执行,只依赖自己包其他的任务,那么可以把依赖的任务放在dependsOn数组里
"dependsOn": ["build", "test", "lint"]
},
"test": {
// A workspace's `test` task depends on that workspace's
// own `build` task being completed first.
"dependsOn": ["build"],
// A workspace's `test` task should only be rerun when
// either a `.tsx` or `.ts` file has changed.
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
},
// A workspace's `lint` task has no dependencies and
// can be run whenever.
// 如果一个任务的dependsOn为[] 或者不声明这个属性,那么表明这个任务可以在任意时间被执行
"lint": {},
// 这里 dev。我们当然希望它不做任何缓存
"dev": {
"cache": false,
// Label a task as persistent if it is a long-running process, such as a dev server or --watch mode. Turbo will prevent other tasks from depending on persistent tasks. Without setting this config, if any other task depends on dev, it will never run, because dev never exits. With this option, turbo can warn you about an invalid configuration.
"persistent": true
}
}
}
最后还要提一下 filter 过滤语法 其实和 pnpm 的类似 比如我们只想用 turbo build admin 项目 。我们就可以
build:admin": "turbo run build --filter=@lacApp/admin",
加上 Turbo,可以在已经很快的构建上,锦上添花。
就这样吧